<template>
  <div class="custom-popover">
    <div class="custom-popover-reference" ref="referenceRef" :class="{'custom-popover-reference--visible':visible}" @mouseenter="handleMouseenter" @mouseleave="handleMouseleave" @click="handleReferenceClick"><slot name="reference"></slot></div>
    <Teleport to="body">
      <div
        v-if="visible"
        :class="[
        'custom-popover-body',
        {'custom-popover-body-placeholder': !positionComputeDone},
        props.customClass]"
        ref="bodyRef"
        :style="{
          top: top + 'px', left: left + 'px', width: props.width ? (typeof props.width === 'number' ? props.width + 'px' : props.width) : 'auto',
        }"
        @mouseenter="handleMouseenter"
        @mouseleave="handleMouseleave"
        @click="handleBodyClick">
        <div class="custom-popover-body-content"><slot></slot></div>
        <span v-if="showArrow" :class="[
          'custom-tooltip-body-arrow',
          { 'custom-tooltip-body-arrow-top': ['top', 'topLeft', 'topRight'].includes(position) },
          { 'custom-tooltip-body-arrow-bottom': ['bottom', 'bottomLeft', 'bottomRight'].includes(position) },
          { 'custom-tooltip-body-arrow-left': ['left', 'leftTop', 'leftBottom'].includes(position) },
          { 'custom-tooltip-body-arrow-right': ['right', 'rightTop', 'rightBottom'].includes(position) }
        ]" :style="{
          top: arrowTop,
          left: arrowLeft,
          bottom: arrowBottom,
          right: arrowRight
        }"></span>
      </div>
    </Teleport>
  </div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'

const emit = defineEmits<{
  (e: 'visible-change', visible: boolean): void
  (e: 'after-enter'): void
  (e: 'after-leave'): void
  (e: 'update:modelValue', value: boolean): void
}>()

const props = withDefaults(defineProps<{
  position?: 'topLeft' | 'top' | 'topRight' | 'leftTop' | 'left' | 'leftBottom' |
    'rightTop' | 'right' | 'rightBottom' | 'bottomLeft' | 'bottom' | 'bottomRight'
  trigger?: 'click' | 'hover'
  getPopupContainer?: (triggerNode: HTMLElement) => Element
  customClass?: string
  offset?: [number, number]
  width?: string | number
  showArrow?: boolean
  stopPropagation?: boolean
  modelValue?: boolean
  visible?: boolean
}>(), {
  position: 'top',
  trigger: 'hover',
  customClass: '',
  offset: () => [0, 0],
  showArrow: true,
  stopPropagation: false,
})

const isParent = (child: HTMLElement | null, parent: HTMLElement): boolean => {
  if (child === null) return false
  if (child === parent) return true
  return isParent(child.parentElement, parent)
}

const gap = 6  /* 间隔 */
const offsetX = props.offset[0]
const offsetY = props.offset[1]
const gutter = 12
const visible = ref(false)
const positionComputeDone = ref(false)
const top = ref(0)
const left = ref(0)
const arrowTop = ref<string | undefined>()
const arrowLeft = ref<string | undefined>()
const arrowBottom = ref<string | undefined>()
const arrowRight = ref<string | undefined>()
// trigger: 'hover'的情况下
// 在 referenceRef 与 bodyRef 之间来回移动，不应该改变visible
const debounceTimer = ref<number>(-1)
const referenceRef = ref<HTMLElement>()
const bodyRef = ref<HTMLElement>()

const listener = (e: any) => {
  if (isParent(e.target, referenceRef.value!)) {
    visible.value = !visible.value
  } else if (!isParent(e.target, bodyRef.value!)) {
    visible.value = false
  }
}
const setPosition = () => {
  if (!referenceRef.value || !bodyRef.value) return
  const referenceRect = referenceRef.value.getBoundingClientRect()
  const bodyRect = bodyRef.value.getBoundingClientRect()
  const referencerectTop = referenceRect.top
  const referencerectLeft = referenceRect.left
  const bodyTop = referencerectTop - bodyRect.height
  let _arrowLeft = 0
  let _arrowTop = 0
  const prefixTopTop = bodyTop - gap
  const prefixBottomTop = referencerectTop + referenceRect.height + gap
  arrowTop.value = undefined
  arrowLeft.value = undefined
  arrowBottom.value = undefined
  arrowRight.value = undefined
  switch(props.position) {
    case 'topLeft':
      left.value = referencerectLeft + offsetX
      top.value = prefixTopTop + offsetY
      _arrowLeft = bodyRect.width / 2 + (referenceRect.width - bodyRect.width) / 2
      if (_arrowLeft > bodyRect.width - gutter) {
        _arrowLeft = bodyRect.width - gutter
      }
      arrowLeft.value = _arrowLeft + 'px'
      break
    case 'top':
      left.value = (referencerectLeft + referenceRect.width / 2 - bodyRect.width / 2) + offsetX
      top.value = prefixTopTop + offsetY
      arrowLeft.value = bodyRect.width / 2 + 'px'
      break
    case 'topRight':
      left.value = referencerectLeft + (referenceRect.width - bodyRect.width) + offsetX
      top.value = prefixTopTop + offsetY
      _arrowLeft = bodyRect.width - referenceRect.width + referenceRect.width / 2
      if (_arrowLeft < gutter) {
        _arrowLeft = gutter
      }
      arrowLeft.value = _arrowLeft + 'px'
      break
    case 'bottomLeft':
      left.value = referencerectLeft + offsetX
      top.value = prefixBottomTop + offsetY
      _arrowLeft = bodyRect.width / 2 + (referenceRect.width - bodyRect.width) / 2
      if (_arrowLeft > bodyRect.width - gutter) {
        _arrowLeft = bodyRect.width - gutter
      }
      arrowLeft.value = _arrowLeft + 'px'
      break
    case 'bottom':
      left.value = (referencerectLeft + referenceRect.width / 2 - bodyRect.width / 2) + offsetX
      top.value = prefixBottomTop + offsetY
      arrowLeft.value = bodyRect.width / 2 + 'px'
      break
    case 'bottomRight':
      left.value = referencerectLeft + (referenceRect.width - bodyRect.width) + offsetX
      top.value = prefixBottomTop + offsetY
      _arrowLeft = bodyRect.width - referenceRect.width + referenceRect.width / 2
      if (_arrowLeft < gutter) {
        _arrowLeft = gutter
      }
      arrowLeft.value = _arrowLeft + 'px'
      break
    case 'leftTop':
      left.value = referencerectLeft - bodyRect.width - gap + offsetX
      top.value = referencerectTop + offsetY
      arrowRight.value = 0 + 'px'
      _arrowTop = referenceRect.height / 2
      if (_arrowTop > bodyRect.height - gutter) {
        _arrowTop = bodyRect.height - gutter
      }
      arrowTop.value = _arrowTop + 'px'
      break
    case 'left':
      left.value = referencerectLeft - bodyRect.width - gap + offsetX
      top.value = referencerectTop + (referenceRect.height - bodyRect.height) / 2 + offsetY
      arrowRight.value = 0 + 'px'
      arrowTop.value = bodyRect.height / 2 + 'px'
      break
    case 'leftBottom':
      left.value = referencerectLeft - bodyRect.width - gap + offsetX
      top.value = referencerectTop - (bodyRect.height - referenceRect.height) + offsetY
      arrowRight.value = 0 + 'px'
      _arrowTop = bodyRect.height - referenceRect.height + referenceRect .height / 2
      _arrowTop = bodyRect.height - referenceRect.height + referenceRect .height / 2
      if (_arrowTop < gutter) {
        _arrowTop = gutter
      }
      arrowTop.value = _arrowTop + 'px'
      break
    case 'rightTop':
      left.value = referencerectLeft + referenceRect.width + gap + offsetX
      top.value = referencerectTop + offsetY
      arrowLeft.value = 0 + 'px'
      _arrowTop = referenceRect.height / 2
      if (_arrowTop > bodyRect.height - gutter) {
        _arrowTop = bodyRect.height - gutter
      }
      arrowTop.value = _arrowTop + 'px'
      break
    case 'right':
      left.value = referencerectLeft + referenceRect.width + gap + offsetX
      top.value = referencerectTop + (referenceRect.height - bodyRect.height) / 2 + offsetY
      arrowLeft.value = 0 + 'px'
      arrowTop.value = bodyRect.height / 2 + 'px'
      break
    case 'rightBottom':
      left.value = referencerectLeft + referenceRect.width + gap + offsetX
      top.value = referencerectTop - (bodyRect.height - referenceRect.height) + offsetY
      arrowLeft.value = 0 + 'px'
      _arrowTop = bodyRect.height - referenceRect.height + referenceRect .height / 2
      if (_arrowTop < gutter) {
        _arrowTop = gutter
      }
      arrowTop.value = _arrowTop + 'px'
      break
  }
  positionComputeDone.value = true
}
const handleVisibleChange = (v: boolean) => {
  emit('visible-change', v)
  if (v) {
    setTimeout(() => {
      setPosition()
      emit('after-enter')
    })
  } else {
    emit('after-leave')
  }
}
const handleMouseenter = () => {
  if (props.trigger !== 'hover') return
  clearTimeout(debounceTimer.value)
  visible.value = true
}
const handleMouseleave = () => {
  if (props.trigger !== 'hover') return
  // 两个元素之间的 enter leave 一般会在 1ms 内完成
  debounceTimer.value = window.setTimeout(() => {
    visible.value = false
  }, 250)
}

const handleReferenceClick = (e: Event) => {
  if (props.trigger === 'click' && props.stopPropagation) {
    e.stopPropagation()
    visible.value = !visible.value
  }
}

const handleBodyClick = (e: Event) => {
  // 只在点击Popover内容区域时阻止冒泡
  if (props.trigger === 'click' && props.stopPropagation) {
    e.stopPropagation()
  }
}

watch(visible, v => {
  handleVisibleChange(v)
  emit('update:modelValue', v)
})

// 监听外部传入的modelValue和visible属性
watch(() => props.modelValue, v => {
  if (v !== undefined && v !== visible.value) {
    visible.value = v
  }
}, { immediate: true })

watch(() => props.visible, v => {
  if (v !== undefined && v !== visible.value) {
    visible.value = v
  }
}, { immediate: true })

onMounted(() => {
  if (props.trigger === 'click') {
    window.addEventListener('click', listener)
  }
})
onBeforeUnmount(() => {
  if (props.trigger === 'click') {
    window.removeEventListener('click', listener)
  }
})
const closePopover = () => {
    if (!visible.value) return
  visible.value = false
}
defineExpose({
  closePopover
})

</script>
<style lang="scss">
:root {
  --arrow-size: 8px;
  --half-arrow-size: calc(-1 * var(--arrow-size) / 2);
}
</style>

<style scoped lang="scss">
.custom-popover-body {
  position: absolute;
  left: 0;
  top: 0;
  padding: 12px 16px;
  border-radius: 4px;
  color: #333;
  box-sizing: border-box;
  box-shadow: 0 4px 10px rgba(0,0,0,.1);
  border: 1px solid rgb(229, 230, 235);
  background-color: #fff;
  z-index: 100;
}
.custom-popover-body.custom-popover-body-placeholder {
  opacity: 0;
}
.custom-popover-reference {
  display: inline-block;
}
.custom-popover-body-content {
  border-radius: 4px;
  overflow: hidden;
}

.custom-tooltip-body-arrow {
  content: "";
  position: absolute;
  border: 1px solid rgb(229, 230, 235);
  width: var(--arrow-size);
  height: var(--arrow-size);
  box-sizing: border-box;
  border-style: solid;
  transform: rotate(45deg);
  background-color: #fff;
  &.custom-tooltip-body-arrow-top {
    bottom: var(--half-arrow-size);
    border-top: none;
    border-left: none;
    margin-left: var(--half-arrow-size);
  }
  &.custom-tooltip-body-arrow-bottom {
    top: var(--half-arrow-size);
    border-bottom: none;
    border-right: none;
    margin-left: var(--half-arrow-size);
  }
  &.custom-tooltip-body-arrow-left {
    border-bottom: none;
    border-left: none;
    margin-right: var(--half-arrow-size);
    margin-top: var(--half-arrow-size);
  }
  &.custom-tooltip-body-arrow-right {
    border-top: none;
    border-right: none;
    margin-left: var(--half-arrow-size);
    margin-top: var(--half-arrow-size);
  }
}
</style>
